/* * Sun Public License Notice * * The contents of this file are subject to the Sun Public License * Version 1.0 (the "License"). You may not use this file except in * compliance with the License. A copy of the License is available at * http://www.sun.com/ * * The Original Code is Forte for Java, Community Edition. The Initial * Developer of the Original Code is Sun Microsystems, Inc. Portions * Copyright 1997-2000 Sun Microsystems, Inc. All Rights Reserved. */ package org.openide.awt; import java.awt.*; import java.awt.event.*; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.Serializable; import java.util.Vector; import javax.swing.*; import javax.swing.event.*; /** ListPane. This component is derived from JList component and enables * list objects in several columns. * * @author Petr Hamernik, Ian Formanek, Jaroslav Tulach */ public class ListPane extends JList { /** generated Serialized Version UID */ static final long serialVersionUID = 3828318151121500783L; private int fixedCellWidth = 100; private int fixedCellHeight = 100; private int visibleRowCount = 6; private int visibleColumnCount = 4; private int realyRowCount = 1; private int realyColumnCount = 1; private int firstVisibleIndex = -1; ListDataListener dataL; PropertyChangeListener propertyL; InputListener inputL; ListSelectionListener selectionL; boolean updateLayoutStateNeeded = true; /** The foreground color of selected items */ private Color selectionForeground = UIManager.getColor("List.selectionForeground"); // NOI18N /** The background color of selected items */ private Color selectionBackground = UIManager.getColor("List.selectionBackground"); // NOI18N /** * Construct a JList that displays the elements in the specified, * non-null model. All JList constructors delegate to this one. */ public ListPane(ListModel dataModel) { super (dataModel); addListListeners(); } /** * Construct a JList that displays the elements in the specified * array. This constructor just delegates to the ListModel * constructor. */ public ListPane(final Object[] listData) { this (new AbstractListModel() { public int getSize() { return listData.length; } public Object getElementAt(int i) { return listData[i]; } }); } /** * Construct a JList that displays the elements in the specified * Vector. This constructor just delegates to the ListModel * constructor. */ public ListPane(final Vector listData) { this (new AbstractListModel() { public int getSize() { return listData.size(); } public Object getElementAt(int i) { return listData.elementAt(i); } }); } /** * Constructs a JList with an empty model. */ public ListPane() { this (new AbstractListModel() { public int getSize() { return 0; } public Object getElementAt(int i) { return null; } }); } /** * JList components are always opaque. * @return true */ public boolean isOpaque() { return true; } /** * Return the value of the visibleRowCount property. * @see #setVisibleRowCount */ public int getVisibleColumnCount() { return visibleColumnCount; } /** * Set the preferred number of rows in the list that are visible within * the nearest JViewport ancestor, if any. The value of this property * only affects the value of the JLists preferredScrollableViewportSize. * <p> * The default value of this property is 8. * <p> * This is a JavaBeans bound property. * * @see #getVisibleRowCount * @see JComponent#getVisibleRect */ public void setVisibleColumnCount(int visibleColumnCount) { int oldValue = this.visibleColumnCount; this.visibleColumnCount = Math.max(0, visibleColumnCount); firePropertyChange("visibleColumnCount", oldValue, visibleColumnCount); // NOI18N } /** * If this JList is being displayed withing a JViewport and the * specified cell isn't completely visible, scroll the viewport. * * @param The index of the cell to make visible * @see JComponent#scrollRectToVisible * @see #getVisibleRect */ public void ensureIndexIsVisible(int index) { Point first = indexToLocation(index); if (first != null) { Rectangle cellBounds = new Rectangle(first.x, first.y, fixedCellWidth, fixedCellHeight); scrollRectToVisible(cellBounds); } } /** * Convert a point in JList coordinates to the index * of the cell at that location. Returns -1 if there's no * cell the specified location. * * @param location The JList relative coordinates of the cell * @return The index of the cell at location, or -1. */ public int locationToIndex(Point location) { int x = location.x / fixedCellWidth; if (x >= realyColumnCount) return -1; int y = location.y / fixedCellHeight; if (y >= realyRowCount) return -1; int ret = y * realyColumnCount + x; return (ret >= getModel ().getSize()) ? -1 : ret; } /** * Returns the origin of the specified item in JList * coordinates, null if index isn't valid. * * @param index The index of the JList cell. * @return The origin of the index'th cell. */ public Point indexToLocation(int index) { if (index >= getModel ().getSize()) return null; int y = index / realyColumnCount; int x = index % realyColumnCount; return new Point(x * fixedCellWidth, y * fixedCellHeight); } /** * Returns the bounds of the specified item in JList * coordinates, null if index isn't valid. * * @param index The index of the JList cell. * @return The bounds of the index'th cell. */ public Rectangle getCellBounds(int index1, int index2) { int minIndex = Math.min(index1, index2); int maxIndex = Math.max(index1, index2); Point p1 = indexToLocation(index1); Point p2 = indexToLocation(index2); int x1 = p1.x; int y1 = p1.y; int x2 = p2.x + fixedCellWidth; int y2 = p2.y + fixedCellHeight; if (p1.y != p2.y) { x1 = 0; x2 = fixedCellWidth * realyColumnCount; } return new Rectangle(x1, y1, x2 - x1, y2 - y1); } /** * --- The Scrollable Implementation --- */ // @see #setPrototypeCellValue /** * Compute the size of the viewport needed to display visibleRowCount * rows. This is trivial if fixedCellWidth and fixedCellHeight * were specified. Note that they can specified implicitly with * the prototypeCellValue property. If fixedCellWidth wasn't specified, * it's computed by finding the widest list element. If fixedCellHeight * wasn't specified then we resort to heuristics: * <ul> * <li> * If the model isn't empty we just multiply the height of the first row * by visibleRowCount. * <li> * If the model is empty, i.e. JList.getModel().getSize() == 0, then * we just allocate 16 pixels per visible row, and 100 pixels * for the width (unless fixedCellWidth was set), and hope for the best. * </ul> * * @see #getPreferredScrollableViewportSize */ public Dimension getPreferredScrollableViewportSize() { Insets insets = getInsets(); int w = insets.left + insets.right + visibleColumnCount * fixedCellWidth; int h = insets.top + insets.bottom + visibleRowCount * fixedCellHeight; Dimension dim = new Dimension(w, h); return dim; } /** * If we're scrolling downwards (<code>direction</code> is * greater than 0), and the first row is completely visible with respect * to <code>visibleRect</code>, then return its height. If * we're scrolling downwards and the first row is only partially visible, * return the height of the visible part of the first row. Similarly * if we're scrolling upwards we return the height of the row above * the first row, unless the first row is partially visible. * * @return The distance to scroll to expose the next or previous row. * @see Scrollable#getScrollableUnitIncrement */ public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) { if (orientation == SwingConstants.HORIZONTAL) { return 1; } else { int row = getFirstVisibleIndex(); if (row == -1) return 0; else { /* Scroll Down */ if (direction > 0) { Rectangle r = getCellBounds(row, row); return (r == null) ? 0 : r.height - (visibleRect.y - r.y); } /* Scroll Up */ else { Rectangle r = getCellBounds(row, row); /* The first row is completely visible and it's row 0. * We're done. */ if ((r.y == visibleRect.y) && (row == 0)) { return 0; } /* The first row is completely visible, return the * height of the previous row. */ else if (r.y == visibleRect.y) { Rectangle prevR = getCellBounds(row - 1, row - 1); return (prevR== null) ? 0 : prevR.height; } /* The first row is partially visible, return the * height of hidden part. */ else { return visibleRect.y - r.y; } } } } } /** * @return The visibleRect.height or visibleRect.width per the orientation. * @see Scrollable#getScrollableUnitIncrement */ public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) { return (orientation == SwingConstants.VERTICAL) ? visibleRect.height : visibleRect.width; } /** * If this JList is displayed in a JViewport, don't change its width * when the viewports width changes. This allows horizontal * scrolling if the JViewport is itself embedded in a JScrollPane. * * @return False - don't track the viewports width. * @see Scrollable#getScrollableTracksViewportWidth */ public boolean getScrollableTracksViewportWidth() { return true; } /** * If this JList is displayed in a JViewport, don't change its height * when the viewports height changes. This allows vertical * scrolling if the JViewport is itself embedded in a JScrollPane. * * @return False - don't track the viewports width. * @see Scrollable#getScrollableTracksViewportWidth */ public boolean getScrollableTracksViewportHeight() { return false; } /** * If the list is opaque, paint its background. * Subclasses may want to override this method rather than paint(). * * @see #paint */ protected void paintBackground(Graphics g) { if (isOpaque()) { Color backup = g.getColor(); g.setColor(getBackground()); g.fillRect(0, 0, getWidth(), getHeight()); g.setColor(backup); } } /** * Paint one List cell: compute the relevant state, get the "rubber stamp" * cell renderer component, and then use the CellRendererPane to paint it. * Subclasses may want to override this method rather than paint(). * * @see #paint */ private void paintCell(Graphics g, int index) { Object value = getModel ().getElementAt(index); boolean cellHasFocus = hasFocus () && (index == getSelectionModel().getLeadSelectionIndex()); boolean isSelected = getSelectionModel ().isSelectedIndex(index); Component renderer = getCellRenderer ().getListCellRendererComponent( this, value, index, isSelected, cellHasFocus ); renderer.setSize(fixedCellWidth, fixedCellHeight); renderer.paint(g); } /** * Paint the rows that intersect the Graphics objects clipRect. This * method calls paintBackground and paintCell as necessary. Subclasses * may want to override these methods. * * @see #paintBackground * @see #paintCell */ protected void paintComponent (Graphics g) { updateLayoutState(); if (getCellRenderer () == null) return; paintBackground(g); /* Compute the area we're going to paint in terms of the affected * rows (firstPaintRow, lastPaintRow), and the clip bounds. */ Rectangle bounds = getVisibleRect(); int first = locationToIndex(new Point(bounds.x, bounds.y)); int last = locationToIndex(new Point(bounds.x + bounds.width, bounds.y + bounds.height)); if (first == -1) first = 0; if (last == -1) last = getModel ().getSize() - 1; Point firstPos = indexToLocation (first); if (firstPos != null) { g.translate (- (bounds.x - firstPos.x), - (bounds.y - firstPos.y)); } int dx, dy; for (int i = first; i <= last; i++) { /* Set the clip rect to be the intersection of rowBounds * and paintBounds and then paint the cell. */ Point p = indexToLocation(i); paintCell(g, i); if (((i+1) % realyColumnCount) == 0) { dx = - fixedCellWidth * (realyColumnCount - 1); dy = fixedCellHeight; } else { dx = fixedCellWidth; dy = 0; } g.translate(dx, dy); } } /** Repaints cells from index0 to index1 */ private void repaintCells(int index0, int index1) { repaint(); /* Graphics g = getGraphics(); if (g == null) return; index0 = Math.max(index0, getFirstVisibleIndex()); index1 = Math.min(index1, getLastVisibleIndex()); if ((index0 == -1) || (index1 == -1)) return; Point p0 = indexToLocation(getFirstVisibleIndex()); Point p1 = indexToLocation(index0); g.translate(p1.x - p0.x, p1.y - p0.y); int dx, dy; for (int i = index0; i <= index1; i++) { paintCell(g, i); if (((i+1) % realyColumnCount) == 0) { dx = - fixedCellWidth * (realyColumnCount - 1); dy = fixedCellHeight; } else { dx = fixedCellWidth; dy = 0; } g.translate(dx, dy); } */ } private void updateLayoutState() { Dimension d = getSize(); int x = d.width / fixedCellWidth; if (x < 1) x = 1; if (x != realyColumnCount) { realyColumnCount = x; updateLayoutStateNeeded = true; } int y = d.height / fixedCellHeight; if (y != realyRowCount) { realyRowCount = y; updateLayoutStateNeeded = true; } firstVisibleIndex = locationToIndex(getVisibleRect().getLocation()); if (updateLayoutStateNeeded) { updateLayoutStateNeeded = false; invalidate(); } } /** * The preferredSize of a list is total height of the rows * and the maximum width of the cells. If JList.fixedCellHeight * is specified then the total height of the rows is just * (cellVerticalMargins + fixedCellHeight) * model.getSize() where * rowVerticalMargins is the space we allocate for drawing * the yellow focus outline. Similarly if JListfixedCellWidth is * specified then we just use that plus the horizontal margins. * * @return The total size of the */ public Dimension getPreferredSize() { Insets insets = getInsets(); int dx = insets.left + insets.right; int dy = insets.top + insets.bottom; int max = getModel().getSize() - 1; if (max <= 0) { return new Dimension(fixedCellWidth, fixedCellHeight); } int y = max / realyColumnCount + 1; int x = ((max <= realyColumnCount) ? max : realyColumnCount); Dimension d = new Dimension(x * fixedCellWidth, y * fixedCellHeight); return d; } /** * @returns The preferred size. * @see #getPreferredSize */ public Dimension getMinimumSize() { return new Dimension(fixedCellWidth, fixedCellHeight); } /** * @returns The preferred size. * @see #getPreferredSize */ public Dimension getMaximumSize() { Dimension d = super.getMaximumSize (); return d; } /** * Create and install the listeners for the JList, its model, and its * selectionModel. This method is called at creation time. */ private void addListListeners() { inputL = createInputListener(); addMouseListener(inputL); addKeyListener(inputL); addFocusListener(inputL); /* When a property changes that effects layout we set * updateLayoutStateNeeded to the appropriate code. We also * add/remove List data model listeners when the "model" * property changes. */ propertyL = createPropertyListener(); addPropertyChangeListener(propertyL); dataL = createDataListener(); ListModel model = getModel(); if (model != null) model.addListDataListener(dataL); if (selectionL == null) { selectionL = new ListSelectionListener() { public void valueChanged(ListSelectionEvent e) { repaint (); /* Rectangle rect = getCellBounds(e.getFirstIndex(), e.getLastIndex()); repaint (rect); // repaintCells(e.getFirstIndex(), e.getLastIndex()); int f = e.getFirstIndex(); int l = e.getLastIndex (); boolean a = e.getValueIsAdjusting(); // fireSelectionValueChanged(f, l, a); */ } }; ListSelectionModel selectionModel = getSelectionModel(); if (selectionModel != null) selectionModel.addListSelectionListener(selectionL); } /* int index = locationToIndex(e.getPoint()); if (index != lastHint) { lastHint = index; setToolTipText(org.openide.explorer.Explorer.findName(dataModel.getElementAt(index)).getName()); } */ } // // ========== Listener inner classes =============== // private InputListener createInputListener() { return new InputListener(); } private ListDataListener createDataListener() { return new DataListener(); } // protected ListSelectionListener createSelectionListener() { // return new SelectionListener(); // } private PropertyChangeListener createPropertyListener() { return new PropertyListener(); } // // ------- Input Listener ------------ // /** * Mouse input, and focus handling for JList. An instance of this * class is added to the appropriate java.awt.Component lists * at creation time. Note keyboard input is handled with JComponent * KeyboardActions, see registerKeyboardActions(). * * @see #createInputListener * @see #registerKeyboardActions */ private class InputListener extends MouseAdapter implements FocusListener, KeyListener, Serializable { static final long serialVersionUID =-7907848327510962576L; transient int dragFirstIndex = -1; transient int dragLastIndex = -1; transient int lastHint = -1; // ==== Mouse methods ===== public void mousePressed(MouseEvent e) { int frk = 0; if (getParent() instanceof JViewport) { frk = ((JViewport)getParent()).getViewPosition().y; } Point pnt = new Point(e.getPoint().x, e.getPoint().y + frk); updateSelection(locationToIndex(pnt), e); //updateSelection(locationToIndex(e.getPoint()), e); if (!hasFocus ()) requestFocus(); } // ==== Focus methods ===== public void focusGained(FocusEvent e) { repaintCellFocus(); } public void focusLost(FocusEvent e) { repaintCellFocus(); } protected void repaintCellFocus() { int i = getLeadSelectionIndex(); repaintCells(i,i); } // ==== Key methods ===== public void keyTyped(KeyEvent e) { } public void keyPressed(KeyEvent e) { int s = getLeadSelectionIndex(); if (s < 0) { if (getModel().getSize() > 0) s = 0; else return; } else { switch (e.getKeyCode()) { case KeyEvent.VK_LEFT: s -= 1; break; case KeyEvent.VK_RIGHT: s += 1; break; case KeyEvent.VK_UP: s -= realyColumnCount; break; case KeyEvent.VK_DOWN: s += realyColumnCount; break; case KeyEvent.VK_HOME: s = 0; break; case KeyEvent.VK_END: s = getModel().getSize() - 1; break; case KeyEvent.VK_PAGE_UP: s -= realyColumnCount * realyRowCount; break; case KeyEvent.VK_PAGE_DOWN: s += realyColumnCount * realyRowCount; break; default: return; } } if (s < 0) s = 0; if (s > getModel().getSize() - 1) s = getModel().getSize() - 1; if(s >= 0) { updateSelection(s, e); } } public void keyReleased(KeyEvent e) { } // ==== Update selection ===== protected void updateSelection(int index, InputEvent e) { if (index != -1) { setValueIsAdjusting(true); if (e.isShiftDown()) { if (e.isControlDown()) { if (dragFirstIndex == -1) addSelectionInterval(index, index); else { if (dragLastIndex == -1) addSelectionInterval(dragFirstIndex, index); else { removeSelectionInterval(dragFirstIndex, dragLastIndex); addSelectionInterval(dragFirstIndex, index); } } } else { if (dragFirstIndex == -1) addSelectionInterval(index, index); else setSelectionInterval(dragFirstIndex, index); } if (dragFirstIndex == -1) { dragFirstIndex = index; dragLastIndex = -1; } else { dragLastIndex = index; } } else { if (e.isControlDown()) { if (isSelectedIndex(index)) removeSelectionInterval(index, index); else addSelectionInterval(index, index); } else { setSelectionInterval(index, index); } dragFirstIndex = index; dragLastIndex = -1; } setValueIsAdjusting(false); } } } // // ------- Data Listener ------------ // /** * The ListDataListener that's added to the JLists model at * creation time, and whenever the JList.model property changes. * * * @see JList#getModel * @see #createDataListener */ private class DataListener implements ListDataListener, Serializable { static final long serialVersionUID =-2252515707418441L; public void intervalAdded(ListDataEvent e) { updateLayoutStateNeeded = true; int minIndex = Math.min(e.getIndex0(), e.getIndex1()); int maxIndex = Math.max(e.getIndex0(), e.getIndex1()); /* Sync the SelectionModel with the DataModel. */ ListSelectionModel sm = getSelectionModel(); if (sm != null) sm.insertIndexInterval(minIndex, maxIndex - minIndex, true); // repaint(); } public void intervalRemoved(ListDataEvent e) { updateLayoutStateNeeded = true; /* Sync the SelectionModel with the DataModel. */ ListSelectionModel sm = getSelectionModel(); if (sm != null) { sm.removeIndexInterval(e.getIndex0(), e.getIndex1()); } // repaint(); } public void contentsChanged(ListDataEvent e) { updateLayoutStateNeeded = true; repaint(); } } // // ------- Property Listener ------------ // /** * The PropertyChangeListener that's added to the JList at * creation time. When the value of a JList property that * affects layout changes, we set a bit in updateLayoutStateNeeded. * If the JLists model changes we additionally remove our listeners * from the old model. Likewise for the JList selectionModel. * * @see #createPropertyListener */ private class PropertyListener implements PropertyChangeListener, Serializable { static final long serialVersionUID =-6765578311995604737L; public void propertyChange(PropertyChangeEvent e) { String propertyName = e.getPropertyName(); if (propertyName.equals("model")) { // NOI18N ListModel oldModel = (ListModel)e.getOldValue(); ListModel newModel = (ListModel)e.getNewValue(); if (oldModel != null) oldModel.removeListDataListener(dataL); if (newModel != null) { newModel.addListDataListener(dataL); updateLayoutStateNeeded = true; repaint(); } else if (propertyName.equals("selectionModel")) { // NOI18N ListSelectionModel oldModelS = (ListSelectionModel)e.getOldValue(); ListSelectionModel newModelS = (ListSelectionModel)e.getNewValue(); if (oldModelS != null) oldModelS.removeListSelectionListener(selectionL); if (newModelS != null) newModelS.addListSelectionListener(selectionL); updateLayoutStateNeeded = true; repaint(); } else if (propertyName.equals("cellRenderer") || // NOI18N propertyName.equals("font") || // NOI18N propertyName.equals("fixedCellHeight") || // NOI18N propertyName.equals("fixedCellWidth")) // NOI18N updateLayoutStateNeeded = true; repaint(); } } } } /* * Log * 12 Gandalf 1.11 1/13/00 Ian Formanek NOI18N * 11 Gandalf 1.10 1/12/00 Ian Formanek NOI18N * 10 Gandalf 1.9 10/22/99 Ian Formanek NO SEMANTIC CHANGE - Sun * Microsystems Copyright in File Comment * 9 Gandalf 1.8 9/28/99 Milan Kubec Correction of position * of mouse event for proper locationToIndex computation * 8 Gandalf 1.7 9/27/99 Martin Balin Updated locationToIndex * to compute proper index * 7 Gandalf 1.6 8/27/99 Jaroslav Tulach New threading model & * Children. * 6 Gandalf 1.5 8/9/99 Ian Formanek Generated Serial Version * UID * 5 Gandalf 1.4 6/8/99 Ian Formanek ---- Package Change To * org.openide ---- * 4 Gandalf 1.3 4/19/99 Jesse Glick [JavaDoc] * 3 Gandalf 1.2 2/4/99 Petr Hamernik * 2 Gandalf 1.1 1/6/99 Ian Formanek Reflecting changes in * location of package "awt" * 1 Gandalf 1.0 1/5/99 Ian Formanek * $ * Beta Change History: * 0 Tuborg 0.11 --/--/98 Jan Formanek method getComponent of CellRenderer renamed to getListPaneCellRendererComponent * 0 Tuborg 0.11 --/--/98 Jan Formanek parameters of this method changed (according to the changes in the interface) * 0 Tuborg 0.12 --/--/98 Jan Formanek removed paintBackground * 0 Tuborg 0.13 --/--/98 Jan Formanek added selectionBackground and selectionForeground properties * 0 Tuborg 0.14 --/--/98 Jan Formanek paintBackground is back (slightly modified) * 0 Tuborg 0.15 --/--/98 Petr Hamernik resize bugfix */